feat(cron): a scheduled job can fire from a calendar or a file change - #260
Conversation
Review — approve, no changesThe framing carries this: "run the release checklist when the release meeting starts" and "regenerate when the schema changes" are both scheduling, and neither is a time. Extending the trigger rather than bolting a watcher onto the side is the right shape. The decisions I'd have argued about are all made the way I'd want:
Choosing |
5445c1e to
81af00c
Compare
Until now the only trigger was a clock. "Run the release checklist when the release meeting starts" and "regenerate the client when the schema changes" are both scheduling, and neither is a time. `TriggerSource` is cron | ics | file. `schedule` still means cron and still works, so no existing job needs migrating — a store nobody has to rewrite cannot be rewritten wrongly. Every source is polled by the `scheduler run` that already exists. No daemon, no watcher process, no way for a trigger to fire while nothing is listening. The cost is minute granularity everywhere, which is the granularity anyone can actually observe: a second-resolution trigger would fire or not depending on how promptly launchd got around to it. ICS is standard calendar text and nothing else — no vendor SDK, no OAuth to a calendar service, no remote account. Every calendar worth integrating with exports `.ics`, and a file on disk is a boundary you can inspect, which a client library is not. The reader handles DTSTART (UTC, floating, all-day), folded SUMMARY lines, and RRULE FREQ=DAILY/WEEKLY with INTERVAL/BYDAY/UNTIL/COUNT. Everything else is *reported*, following the file-contract parser's rule: a silently ignored RRULE is a job that never fires, and that failure is indistinguishable from "nothing was scheduled". TZID is reported rather than honoured — there is no timezone database here, and applying the host's zone would make one file fire at different moments on different machines. All-day entries never fire. They name a day, not a moment, and picking one would be this module inventing a schedule the user did not write. A file trigger's first evaluation records a baseline instead of firing, or every one of them would go off the moment it was created on files nobody had touched. The scheduler stamps that baseline itself — without it the job could never acquire one and would stay silent forever while looking configured. A trigger decides when, never what may happen: scheduled runs still go through the unattended clamp. A calendar you do not control deciding when DeepCode runs is already worth thinking about; it must not also decide what it may do. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
81af00c to
027afa8
Compare
Note from the rebase onto
|
Closes the 未做 item in
docs/FLOATBOAT_ADOPTION_PLAN.md§4.1: "触发源抽象(ICS / watch)… 未做。cron仍只有时间源".The gap
Until now the only trigger was a clock. "Run the release checklist when the release meeting starts" and "regenerate the client when the schema changes" are both scheduling, and neither is a time.
{ "id": "release-prep", "trigger": { "kind": "ics", "path": "team.ics", "match": "release" }, … } { "id": "regen", "trigger": { "kind": "file", "paths": ["schema.json"] }, … }schedulestill means cron and still works. No existing job is migrated — a store nobody has to rewrite cannot be rewritten wrongly.Everything is polled
scheduler runalready wakes on a timer and asks what is due; every source answers that same question. No daemon, no watcher process, no way for a trigger to fire while nothing is listening.The cost is minute granularity everywhere — which is the granularity anyone can observe anyway, since a second-resolution trigger would fire or not depending on how promptly launchd got around to it.
ICS: standard text, nothing else
No vendor SDK, no OAuth to a calendar service, no remote account polling. Every calendar worth integrating with exports
.ics, and a file on disk is a boundary you can inspect — which a client library is not.DTSTARTUTC / floating / all-daySUMMARYlinesRRULE FREQ=DAILY/WEEKLY+INTERVAL/BYDAY/UNTIL/COUNTThat last row is the important one, and it follows the file-contract parser's rule: a silently ignored
RRULEis a job that never fires, and that failure is indistinguishable from "nothing was scheduled".FREQ=MONTHLY,BYDAY=2MO,BYMONTHDAY,TZID— each is logged next to the job that hit it.TZIDis reported rather than honoured: there is no timezone database here, and applying the host's zone would make one file fire at different moments on different machines.All-day entries never fire. They name a day, not a moment, and picking one (midnight? 09:00?) would be inventing a schedule the user did not write.
File triggers have two deliberate behaviours
Permissions are untouched
A trigger decides when, never what may happen. Scheduled runs still go through the unattended clamp from #244. A calendar you do not control deciding when DeepCode runs is already worth thinking about; it must not also decide what it may do.
Verification
typecheck, lint, format, docs clean; full suite green. 48 new tests, weighted at the recurrence arithmetic, which is where this is most likely to be subtly wrong: DAILY/WEEKLY interval, BYDAY, UNTIL, COUNT (counting the first occurrence, and counting per-BYDAY within a week), time-of-day preservation, and not matching before the first occurrence.
🤖 Generated with Claude Code